using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Babylon.Importers;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;

// TODO: replace this with the type you want to import.
using TImport = Microsoft.Xna.Framework.Content.Pipeline.Graphics.NodeContent;

namespace ContentPipelineExtensions.Babylon
{
    /// <summary>
    /// This class will be instantiated by the XNA Framework Content Pipeline
    /// to import a file from disk into the specified type, TImport.
    /// 
    /// This should be part of a Content Pipeline Extension Library project.
    /// 
    /// TODO: change the ContentImporter attribute to specify the correct file
    /// extension, display name, and default processor for this importer.
    /// </summary>
    [ContentImporter(".obj", DisplayName = "OBJ Importer for Viper", DefaultProcessor = "OBJ Importer for Viper")]
    public class ObjModelImporter : ContentImporter<TImport>
    {
        private NodeContent _rootNode;
        private string _fileName;

        Dictionary<string, EffectMaterialContent> _materials = new Dictionary<string, EffectMaterialContent>();

        public override TImport Import(string filename, ContentImporterContext context)
        {
            _fileName = filename;
            _rootNode = new NodeContent() { Identity = new ContentIdentity(filename) };
            var document = new Document<ObjLine>(File.OpenRead(filename));

            ProcessObjDocument(document);
            ConvertFaceGroupsToMeshAndMeshParts();
            return _rootNode;
        }

        string HashVertex(VertexPositionNormalTexture v){
            return string.Format("{0}|{1}|{2}|{3}|{4}|{5}|{6}|{7}", v.Position.X, v.Position.Y, v.Position.Z, v.Normal.X,
                                 v.Normal.Y, v.Normal.Z, v.TextureCoordinate.X, v.TextureCoordinate.Y);
        }

        int GetIndexOrAdd(List<VertexPositionNormalTexture> list, Dictionary<string,int> hashDirectory,  VertexPositionNormalTexture vertex){
            var hash = HashVertex(vertex);
            int index;
            if (!hashDirectory.TryGetValue(hash, out index)){
                index = list.Count;
                list.Add(vertex);
                hashDirectory.Add(hash, index);
            }
            return index;
        }
        private void ConvertFaceGroupsToMeshAndMeshParts(){
            MeshContent[] meshContents = new MeshContent[_faceGroups.Count];

            Parallel.For(0, meshContents.Length, (faceGroupIndex) => {
                var faceGroup = _faceGroups[faceGroupIndex];
                MeshBuilder mesh = MeshBuilder.StartMesh(faceGroup.GroupName);

                List<VertexPositionNormalTexture> vertices = new List<VertexPositionNormalTexture>();
                Dictionary<string, int> hashDirectory = new Dictionary<string, int>();
                List<int> indices = new List<int>();
                foreach (var face in faceGroup.Faces)
                {

                    VertexPositionNormalTexture v1 = new VertexPositionNormalTexture(_positions[face.V1.PositionIndex],
                                                                 _normals[face.V1.NormalIndex],
                                                                 _texCoords[face.V1.TexCoordIndex]);


                    VertexPositionNormalTexture v2 = new VertexPositionNormalTexture(_positions[face.V2.PositionIndex],
                                                                 _normals[face.V2.NormalIndex],
                                                                 _texCoords[face.V2.TexCoordIndex]);


                    VertexPositionNormalTexture v3 = new VertexPositionNormalTexture(_positions[face.V3.PositionIndex],
                                                                 _normals[face.V3.NormalIndex],
                                                                 _texCoords[face.V3.TexCoordIndex]);

                    indices.Add(GetIndexOrAdd(vertices,hashDirectory, v1));
                    indices.Add(GetIndexOrAdd(vertices, hashDirectory, v2));
                    indices.Add(GetIndexOrAdd(vertices, hashDirectory, v3));
                }
                

                for (int i = 0; i < vertices.Count; i++)
                {
                    mesh.CreatePosition(vertices[i].Position);
                }



                mesh.SetMaterial(_materials[faceGroup.Material]);
                var normalChannel = mesh.CreateVertexChannel<Vector3>(VertexChannelNames.Normal(0));
                var texChannel = mesh.CreateVertexChannel<Vector2>(VertexChannelNames.TextureCoordinate(0));
                int currentStep = 0;
                var opaqueData = new OpaqueDataDictionary();
                opaqueData.Add("step", currentStep);
                mesh.SetOpaqueData(opaqueData);
                for(int i = 0; i<indices.Count; i++)
                {
                    int mod = i % 90000;
                    if (mod == 0)
                    {
                        currentStep++;
                        opaqueData = new OpaqueDataDictionary();
                        opaqueData.Add("step", currentStep);
                        mesh.SetOpaqueData(opaqueData);
                    }
                    var index = indices[i];
                    mesh.SetVertexChannelData(normalChannel, vertices[index].Normal);
                    mesh.SetVertexChannelData(texChannel, vertices[index].TextureCoordinate);
                    mesh.AddTriangleVertex(index);
                }

                MeshContent meshContent = mesh.FinishMesh();

                meshContents[faceGroupIndex] = meshContent;
            });

            foreach (var content in meshContents)
            {
                _rootNode.Children.Add(content);
            }
            
        }

        private struct Face
        {
            private readonly VertexRef _v1;
            private readonly VertexRef _v2;
            private readonly VertexRef _v3;

            public Face(VertexRef v1, VertexRef v2, VertexRef v3)
            {
                _v1 = v1;
                _v2 = v2;
                _v3 = v3;
            }

            public VertexRef V3
            {
                get { return _v3; }
            }

            public VertexRef V2
            {
                get { return _v2; }
            }

            public VertexRef V1
            {
                get { return _v1; }
            }

            // override object.Equals
            public override bool Equals(object obj)
            {
                //       
                // See the full list of guidelines at
                //   http://go.microsoft.com/fwlink/?LinkID=85237  
                // and also the guidance for operator== at
                //   http://go.microsoft.com/fwlink/?LinkId=85238
                //

                if (obj == null || GetType() != obj.GetType())
                {
                    return false;
                }

                var other = (Face)obj;
                return this._v1 == other._v1
                       && _v2 == other._v2
                       && _v3 == other._v3;
            }

            public static bool operator ==(Face left, Face right)
            {
                return left._v1 == right._v1
                       && left._v2 == right._v2
                       && left._v3 == right._v3;
            }

            public static bool operator !=(Face left, Face right)
            {
                return left._v1 != right._v1
                       || left._v2 != right._v2
                       || left._v3 != right._v3;
            }

            // override object.GetHashCode
            public override int GetHashCode()
            {
                return (int)((_v1.GetHashCode() << 20) + (_v2.GetHashCode() << 10) + _v3.GetHashCode());
            }
        }
        private struct VertexRef
        {
            private readonly int _positionIndex;
            private readonly int _normalIndex;
            private readonly int _texCoordIndex;

            public VertexRef(int positionIndex, int normalIndex, int texCoordIndex)
            {
                _positionIndex = positionIndex;
                _normalIndex = normalIndex;
                _texCoordIndex = texCoordIndex;
            }

            public int TexCoordIndex
            {
                get { return _texCoordIndex; }
            }

            public int NormalIndex
            {
                get { return _normalIndex; }
            }

            public int PositionIndex
            {
                get { return _positionIndex; }
            }

            // override object.Equals
            public override bool Equals(object obj)
            {
                //       
                // See the full list of guidelines at
                //   http://go.microsoft.com/fwlink/?LinkID=85237  
                // and also the guidance for operator== at
                //   http://go.microsoft.com/fwlink/?LinkId=85238
                //

                if (obj == null || GetType() != obj.GetType())
                {
                    return false;
                }

                var other = (VertexRef)obj;
                return this._positionIndex == other._positionIndex
                       && _normalIndex == other._normalIndex
                       && _texCoordIndex == other._texCoordIndex;
            }

            public static bool operator ==(VertexRef left, VertexRef right)
            {
                return left._positionIndex == right._positionIndex
                       && left._normalIndex == right._normalIndex
                       && left._texCoordIndex == right._texCoordIndex;
            }

            public static bool operator !=(VertexRef left, VertexRef right)
            {
                return left._positionIndex != right._positionIndex
                       || left._normalIndex != right._normalIndex
                       || left._texCoordIndex != right._texCoordIndex;
            }

            // override object.GetHashCode
            public override int GetHashCode()
            {
                return (int)((_positionIndex << 20) + (_normalIndex << 10) + _texCoordIndex);
            }
        }

        private class FaceGroup
        {
            private readonly List<Face> _faces;
            private readonly string _groupName;
            private readonly string _material;

            public FaceGroup(List<Face> faces, string groupName, string material)
            {
                _faces = faces;
                _groupName = groupName;
                _material = material;
            }

            public string Material
            {
                get { return _material; }
            }

            public string GroupName
            {
                get { return _groupName; }
            }

            public List<Face> Faces
            {
                get { return _faces; }
            }
        }

        private string _currentGroupName;

        List<Vector3> _positions = new List<Vector3>();
        List<Vector3> _normals = new List<Vector3>();
        List<Vector2> _texCoords = new List<Vector2>();
        List<FaceGroup> _faceGroups = new List<FaceGroup>();
        List<Face> _currentFaces = new List<Face>();
        private string _currentMaterial;

        VertexRef GetVertexRef(int index, ObjLine line)
        {
            string[] indices = line.Tokens[index].Split('/');

            // Required: Position
            int positionIndex = int.Parse(indices[0], CultureInfo.InvariantCulture);
            int texCoordIndex = -1;
            int normalIndex = -1;

            // Optional: Texture coordinate
            if (indices.Length > 1 && indices[1].Equals(string.Empty) == false)
            {
                texCoordIndex = int.Parse(indices[1]);
            }

            // Optional: Normal
            if (indices.Length > 2 && indices[2].Equals(string.Empty) == false)
            {
                normalIndex = int.Parse(indices[2]);
            }

            var vertexRef = new VertexRef(positionIndex-1, normalIndex-1, texCoordIndex-1);
            return vertexRef;
        }
        void AppendFace(ObjLine line)
        {
            // Line
            if (line.Tokens.Length == 3)
            {
                var v1 = GetVertexRef(1, line);
                var v2 = GetVertexRef(2, line);
                var v3 = GetVertexRef(2, line);
                var face = new Face(v1, v2, v3);
                AddFaceIfNotAlreadyThere(face);
                return;
                
            }

            // Triangle
            if (line.Tokens.Length == 4)
            {

                var v1 = GetVertexRef(1, line);
                var v2 = GetVertexRef(2, line);
                var v3 = GetVertexRef(3, line);
                var face = new Face(v1, v2, v3);
                AddFaceIfNotAlreadyThere(face);
                return;
            }

            // Quad
            if (line.Tokens.Length == 5)
            {

                var v1 = GetVertexRef(1, line);
                var v2 = GetVertexRef(2, line);
                var v3 = GetVertexRef(3, line);
                var v4 = GetVertexRef(4, line);
                var face = new Face(v1, v2, v3);
                var face2 = new Face(v1, v3, v4);
                AddFaceIfNotAlreadyThere(face);
                AddFaceIfNotAlreadyThere(face2);
                return;
            }
        }

        private void AddFaceIfNotAlreadyThere(Face face){
            _currentFaces.Add(face);
        }

        private void ProcessObjDocument(Document<ObjLine> document)
        {
            foreach (var lines in document.Blocks)
            {
                foreach (ObjLine line in lines)
                {
                    //currentProgression++;
                    //ReportProgressChanged(50 + (currentProgression * 50) / total);

                    switch (line.Header)
                    {
                        case ObjHeader.Vertices:
                            _positions.Add(line.ToVector3());
                            break;
                        case ObjHeader.TextureCoordinates:
                            Vector2 tv = line.ToVector2();
                            tv.Y = 1.0f - tv.Y;

                            _texCoords.Add(tv);
                            break;
                        case ObjHeader.Normals:
                            _normals.Add(line.ToVector3());
                            break;
                        case ObjHeader.Group:
                            _currentGroupName = string.Join(" ", line.Tokens, 1, line.Tokens.Length - 1);
                            break;
                        case ObjHeader.Faces:
                            AppendFace(line);
                            break;
                        case ObjHeader.MaterialLibrary:
                            ImportMaterialLibraries(line);
                            break;
                        case ObjHeader.Material:
                            if (_currentMaterial != null)
                            {
                                _faceGroups.Add(new FaceGroup(_currentFaces, _currentGroupName, _currentMaterial));
                                _currentFaces = new List<Face>();
                            }
                            _currentMaterial = line.Tokens[1];
                            break;
                        //}
                    }
                }

                //FreezeCurrentMesh();
                //result.Freeze();
            }

            if (_currentMaterial != null)
            {
                _faceGroups.Add(new FaceGroup(_currentFaces, _currentGroupName, _currentMaterial));
                _currentFaces = new List<Face>();
            }
            MergeGroupsByMaterial();
        }

        private void ImportMaterialLibraries(ObjLine materialLine)
        {
            for (int i = 1; i < materialLine.Tokens.Length; i++)
            {
                string fileName = materialLine.Tokens[i];

                Stream source = File.OpenRead(Path.Combine(Path.GetDirectoryName(_fileName), fileName));

                Document<MtlLine> mtlDocument = new Document<MtlLine>(source);


                int currentProgression = 0;

                int total = mtlDocument.Blocks.Sum(lines => lines.Count);
                int scale = 50 / materialLine.Tokens.Length;

                EffectMaterialContent currentEffect = null;
                string direcoty = Path.GetDirectoryName(_fileName);

                foreach (var lines in mtlDocument.Blocks)
                {
                    foreach (MtlLine line in lines)
                    {

                        switch (line.Header)
                        {
                            case MtlHeader.Material:
                                currentEffect = new EffectMaterialContent { Identity = new ContentIdentity(line.Tokens[1]) };
                                _materials.Add(line.Tokens[i], currentEffect);
                                break;
                            case MtlHeader.DiffuseColor:
                                currentEffect.OpaqueData.Add("DiffuseColor", line.ToColor().ToVector4());
                                break;
                            case MtlHeader.DiffuseTexture:
                                {
                                    string locator = line.Tokens[1].Replace("//", @"\");
                                    currentEffect.Textures.Add("DiffuseTexture", new ExternalReference<TextureContent>(Path.Combine(direcoty, locator)));
                                }
                                break;
                            case MtlHeader.Alpha:
                                currentEffect.OpaqueData.Add("Alpha", line.ToFloat());
                                break;
                            case MtlHeader.EmissiveColor:
                                currentEffect.OpaqueData.Add("EmissiveColor", line.ToColor().ToVector3());
                                break;
                            case MtlHeader.SpecularColor:
                                currentEffect.OpaqueData.Add("SpecularColor", line.ToColor().ToVector4());
                                break;
                            case MtlHeader.SpecularPower:
                                currentEffect.OpaqueData.Add("SpecularPower", line.ToFloat());
                                break;
                            case MtlHeader.SpecularTexture:
                                {
                                    string locator = line.Tokens[1].Replace("//", @"\");
                                    currentEffect.Textures.Add("SpecularMap", new ExternalReference<TextureContent>(Path.Combine(direcoty, locator)));
                                }
                                break;
                            case MtlHeader.AmbientColor:
                                currentEffect.OpaqueData.Add("AmbientColor", line.ToColor().ToVector3());
                                break;
                            case MtlHeader.BumpTexture:
                                {
                                    string locator = line.Tokens[1].Replace("//", @"\");
                                    currentEffect.Textures.Add("NormalMap",
                                                                 new ExternalReference<TextureContent>(Path.Combine(direcoty, locator)));
                                }
                                break;
                        }
                    }
                }
            }    
        }


        private void MergeGroupsByMaterial(){
            List<FaceGroup> finalFaceGroup = new List<FaceGroup>();
            var materialGroups = _faceGroups.GroupBy(g => g.Material);
            foreach (var materialGroup in materialGroups){
                finalFaceGroup.Add(MergeFaceGroups(materialGroup));
            }
            _faceGroups = finalFaceGroup;
        }

        private FaceGroup MergeFaceGroups(IGrouping<string, FaceGroup> materialGroup){
            string groupName = string.Join("+", materialGroup.Select(g => g.GroupName));
            string material = materialGroup.Key;
            List<Face> faces = materialGroup.SelectMany(g => g.Faces).Distinct().ToList();

            int oldCount = materialGroup.Sum(g => g.Faces.Count);
            int newCount = faces.Count;
            var elliminated = oldCount - newCount;
            Debug.WriteLine("eliminated faces for group {0} : {1}", material, elliminated);

            return new FaceGroup(faces, groupName, material);
        }
    }
}